// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Query.Internal
{
    /// <summary>
    ///     This is an internal API that supports the Entity Framework Core infrastructure and not subject to
    ///     the same compatibility standards as public APIs. It may be changed or removed without notice in
    ///     any release. You should only use it directly in your code with extreme caution and knowing that
    ///     doing so can result in application failures when updating to a new Entity Framework Core release.
    /// </summary>
    public class BufferedDataReader : DbDataReader
    {
        private DbDataReader _underlyingReader;
        private List<BufferedDataRecord> _bufferedDataRecords = new List<BufferedDataRecord>();
        private BufferedDataRecord _currentResultSet;
        private int _currentResultSetNumber;
        private int _recordsAffected;
        private bool _disposed;
        private bool _isClosed;

        public BufferedDataReader([NotNull] DbDataReader reader)
        {
            _underlyingReader = reader;
        }

        public override int RecordsAffected => _recordsAffected;

        public override object this[string name] => throw new NotSupportedException();

        public override object this[int ordinal] => throw new NotSupportedException();

        public override int Depth => throw new NotSupportedException();

        public override int FieldCount
        {
            get
            {
                AssertReaderIsOpen();
                return _currentResultSet.FieldCount;
            }
        }

        public override bool HasRows
        {
            get
            {
                AssertReaderIsOpen();
                return _currentResultSet.HasRows;
            }
        }

        public override bool IsClosed => _isClosed;

        [Conditional("DEBUG")]
        private void AssertReaderIsOpen()
        {
            if (_underlyingReader != null)
            {
                throw new InvalidOperationException("The reader wasn't initialized");
            }

            if (_isClosed)
            {
                throw new InvalidOperationException("The reader is closed.");
            }
        }

        [Conditional("DEBUG")]
        private void AssertReaderIsOpenWithData()
        {
            AssertReaderIsOpen();

            if (!_currentResultSet.IsDataReady)
            {
                throw new InvalidOperationException("The reader doesn't have any data.");
            }
        }

        [Conditional("DEBUG")]
        private void AssertFieldIsReady(int ordinal)
        {
            AssertReaderIsOpenWithData();

            if (0 > ordinal
                || ordinal > _currentResultSet.FieldCount)
            {
                throw new IndexOutOfRangeException();
            }
        }

        public virtual BufferedDataReader Initialize([NotNull] IReadOnlyList<ReaderColumn> columns)
        {
            if (_underlyingReader == null)
            {
                return this;
            }

            try
            {
                do
                {
                    _bufferedDataRecords.Add(new BufferedDataRecord().Initialize(_underlyingReader, columns));
                }
                while (_underlyingReader.NextResult());

                _recordsAffected = _underlyingReader.RecordsAffected;
                _currentResultSet = _bufferedDataRecords[_currentResultSetNumber];

                return this;
            }
            finally
            {
                _underlyingReader.Dispose();
                _underlyingReader = null;
            }
        }

        public virtual async Task<BufferedDataReader> InitializeAsync(
            [NotNull] IReadOnlyList<ReaderColumn> columns, CancellationToken cancellationToken)
        {
            if (_underlyingReader == null)
            {
                return this;
            }

            try
            {
                do
                {
                    _bufferedDataRecords.Add(await new BufferedDataRecord().InitializeAsync(_underlyingReader, columns, cancellationToken));
                }
                while (await _underlyingReader.NextResultAsync(cancellationToken));

                _recordsAffected = _underlyingReader.RecordsAffected;
                _currentResultSet = _bufferedDataRecords[_currentResultSetNumber];

                return this;
            }
            finally
            {
                _underlyingReader.Dispose();
                _underlyingReader = null;
            }
        }

        public static bool IsSupportedValueType(Type type)
            => type == typeof(int)
                || type == typeof(bool)
                || type == typeof(Guid)
                || type == typeof(byte)
                || type == typeof(char)
                || type == typeof(DateTime)
                || type == typeof(DateTimeOffset)
                || type == typeof(decimal)
                || type == typeof(double)
                || type == typeof(float)
                || type == typeof(short)
                || type == typeof(long)
                || type == typeof(uint)
                || type == typeof(ushort)
                || type == typeof(ulong)
                || type == typeof(sbyte);

        public override void Close()
        {
            _bufferedDataRecords = null;
            _isClosed = true;

            var reader = _underlyingReader;
            if (reader != null)
            {
                _underlyingReader = null;
                reader.Dispose();
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (!_disposed
                && disposing
                && !IsClosed)
            {
                Close();
            }

            _disposed = true;

            base.Dispose(disposing);
        }

        public override bool GetBoolean(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetBoolean(ordinal);
        }

        public override byte GetByte(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetByte(ordinal);
        }

        public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length)
        {
            throw new NotSupportedException();
        }

        public override char GetChar(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetChar(ordinal);
        }

        public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length)
        {
            throw new NotSupportedException();
        }

        public override DateTime GetDateTime(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetDateTime(ordinal);
        }

        public override decimal GetDecimal(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetDecimal(ordinal);
        }

        public override double GetDouble(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetDouble(ordinal);
        }

        public override float GetFloat(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetFloat(ordinal);
        }

        public override Guid GetGuid(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetGuid(ordinal);
        }

        public override short GetInt16(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetInt16(ordinal);
        }

        public override int GetInt32(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetInt32(ordinal);
        }

        public override long GetInt64(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetInt64(ordinal);
        }

        public override string GetString(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetFieldValue<string>(ordinal);
        }

        public override T GetFieldValue<T>(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetFieldValue<T>(ordinal);
        }

        public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetFieldValueAsync<T>(ordinal, cancellationToken);
        }

        public override object GetValue(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.GetValue(ordinal);
        }

        public override int GetValues(object[] values)
        {
            AssertReaderIsOpenWithData();
            return _currentResultSet.GetValues(values);
        }

        public override string GetDataTypeName(int ordinal)
        {
            AssertReaderIsOpen();
            return _currentResultSet.GetDataTypeName(ordinal);
        }

        public override Type GetFieldType(int ordinal)
        {
            AssertReaderIsOpen();
            return _currentResultSet.GetFieldType(ordinal);
        }

        public override string GetName(int ordinal)
        {
            AssertReaderIsOpen();
            return _currentResultSet.GetName(ordinal);
        }

        public override int GetOrdinal(string name)
        {
            Check.NotNull(name, "name");
            AssertReaderIsOpen();
            return _currentResultSet.GetOrdinal(name);
        }

        public override bool IsDBNull(int ordinal)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.IsDBNull(ordinal);
        }

        public override Task<bool> IsDBNullAsync(int ordinal, CancellationToken cancellationToken)
        {
            AssertFieldIsReady(ordinal);
            return _currentResultSet.IsDBNullAsync(ordinal, cancellationToken);
        }

        public override IEnumerator GetEnumerator() => throw new NotSupportedException();

        public override DataTable GetSchemaTable() => throw new NotSupportedException();

        public override bool NextResult()
        {
            AssertReaderIsOpen();
            if (++_currentResultSetNumber < _bufferedDataRecords.Count)
            {
                _currentResultSet = _bufferedDataRecords[_currentResultSetNumber];
                return true;
            }

            _currentResultSet = null;
            return false;
        }

        public override Task<bool> NextResultAsync(CancellationToken cancellationToken)
            => Task.FromResult(NextResult());

        public override bool Read()
        {
            AssertReaderIsOpen();
            return _currentResultSet.Read();
        }

        public override Task<bool> ReadAsync(CancellationToken cancellationToken)
        {
            AssertReaderIsOpen();
            return _currentResultSet.ReadAsync(cancellationToken);
        }

        private class BufferedDataRecord
        {
            private int _currentRowNumber = -1;
            private int _rowCount;
            private string[] _dataTypeNames;
            private Type[] _fieldTypes;
            private string[] _columnNames;
            private Lazy<Dictionary<string, int>> _fieldNameLookup;

            private int _rowCapacity = 1;

            // Resizing bool[] is faster than BitArray, but the latter is more efficient for long-term storage.
            private BitArray _bools;
            private bool[] _tempBools;
            private int _boolCount;
            private byte[] _bytes;
            private int _byteCount;
            private char[] _chars;
            private int _charCount;
            private DateTime[] _dateTimes;
            private int _dateTimeCount;
            private DateTimeOffset[] _dateTimeOffsets;
            private int _dateTimeOffsetCount;
            private decimal[] _decimals;
            private int _decimalCount;
            private double[] _doubles;
            private int _doubleCount;
            private float[] _floats;
            private int _floatCount;
            private Guid[] _guids;
            private int _guidCount;
            private short[] _shorts;
            private int _shortCount;
            private int[] _ints;
            private int _intCount;
            private long[] _longs;
            private int _longCount;
            private sbyte[] _sbytes;
            private int _sbyteCount;
            private uint[] _uints;
            private int _uintCount;
            private ushort[] _ushorts;
            private int _ushortCount;
            private ulong[] _ulongs;
            private int _ulongCount;
            private object[] _objects;
            private int _objectCount;
            private int[] _ordinalToIndexMap;

            private BitArray _nulls;
            private bool[] _tempNulls;
            private int _nullCount;
            private int[] _nullOrdinalToIndexMap;

            private TypeCase[] _columnTypeCases;

            private DbDataReader _underlyingReader;
            private IReadOnlyList<ReaderColumn> _columns;
            private int[] _indexMap;

            public bool IsDataReady { get; private set; }

            public bool HasRows => _rowCount > 0;

            public int FieldCount => _fieldTypes.Length;

            public string GetDataTypeName(int ordinal) => _dataTypeNames[ordinal];

            public Type GetFieldType(int ordinal) => _fieldTypes[ordinal];

            public string GetName(int ordinal) => _columnNames[ordinal];

            public int GetOrdinal(string name) => _fieldNameLookup.Value[name];

            public bool GetBoolean(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Bool
                    ? _bools[_currentRowNumber * _boolCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<bool>(ordinal);

            public byte GetByte(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Byte
                    ? _bytes[_currentRowNumber * _byteCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<byte>(ordinal);

            public char GetChar(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Char
                    ? _chars[_currentRowNumber * _charCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<char>(ordinal);

            public DateTime GetDateTime(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.DateTime
                    ? _dateTimes[_currentRowNumber * _dateTimeCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<DateTime>(ordinal);

            public DateTimeOffset GetDateTimeOffset(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.DateTimeOffset
                    ? _dateTimeOffsets[_currentRowNumber * _dateTimeOffsetCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<DateTimeOffset>(ordinal);

            public decimal GetDecimal(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Decimal
                    ? _decimals[_currentRowNumber * _decimalCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<decimal>(ordinal);

            public double GetDouble(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Double
                    ? _doubles[_currentRowNumber * _doubleCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<double>(ordinal);

            public float GetFloat(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Float
                    ? _floats[_currentRowNumber * _floatCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<float>(ordinal);

            public Guid GetGuid(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Guid
                    ? _guids[_currentRowNumber * _guidCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<Guid>(ordinal);

            public short GetInt16(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Short
                    ? _shorts[_currentRowNumber * _shortCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<short>(ordinal);

            public int GetInt32(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Int
                    ? _ints[_currentRowNumber * _intCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<int>(ordinal);

            public long GetInt64(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.Long
                    ? _longs[_currentRowNumber * _longCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<long>(ordinal);

            public sbyte GetSByte(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.SByte
                    ? _sbytes[_currentRowNumber * _sbyteCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<sbyte>(ordinal);

            public ushort GetUInt16(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.UShort
                    ? _ushorts[_currentRowNumber * _ushortCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<ushort>(ordinal);

            public uint GetUInt32(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.UInt
                    ? _uints[_currentRowNumber * _uintCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<uint>(ordinal);

            public ulong GetUInt64(int ordinal)
                => _columnTypeCases[ordinal] == TypeCase.ULong
                    ? _ulongs[_currentRowNumber * _ulongCount + _ordinalToIndexMap[ordinal]]
                    : GetFieldValue<ulong>(ordinal);

            public object GetValue(int ordinal)
                => GetFieldValue<object>(ordinal);

            public int GetValues(object[] values)
                => throw new NotSupportedException();

            public T GetFieldValue<T>(int ordinal)
            {
                switch (_columnTypeCases[ordinal])
                {
                    case TypeCase.Bool:
                        return (T)(object)GetBoolean(ordinal);
                    case TypeCase.Byte:
                        return (T)(object)GetByte(ordinal);
                    case TypeCase.Char:
                        return (T)(object)GetChar(ordinal);
                    case TypeCase.DateTime:
                        return (T)(object)GetDateTime(ordinal);
                    case TypeCase.DateTimeOffset:
                        return (T)(object)GetDateTimeOffset(ordinal);
                    case TypeCase.Decimal:
                        return (T)(object)GetDecimal(ordinal);
                    case TypeCase.Double:
                        return (T)(object)GetDouble(ordinal);
                    case TypeCase.Float:
                        return (T)(object)GetFloat(ordinal);
                    case TypeCase.Guid:
                        return (T)(object)GetGuid(ordinal);
                    case TypeCase.Short:
                        return (T)(object)GetInt16(ordinal);
                    case TypeCase.Int:
                        return (T)(object)GetInt32(ordinal);
                    case TypeCase.Long:
                        return (T)(object)GetInt64(ordinal);
                    case TypeCase.SByte:
                        return (T)(object)GetSByte(ordinal);
                    case TypeCase.UShort:
                        return (T)(object)GetUInt16(ordinal);
                    case TypeCase.UInt:
                        return (T)(object)GetUInt32(ordinal);
                    case TypeCase.ULong:
                        return (T)(object)GetUInt64(ordinal);
                    case TypeCase.Empty:
                        return default;
                    default:
                        return (T)_objects[_currentRowNumber * _objectCount + _ordinalToIndexMap[ordinal]];
                }
            }

            public Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken)
                => Task.FromResult(GetFieldValue<T>(ordinal));

            public bool IsDBNull(int ordinal) => _nulls[_currentRowNumber * _nullCount + _nullOrdinalToIndexMap[ordinal]];

            public Task<bool> IsDBNullAsync(int ordinal, CancellationToken cancellationToken) => Task.FromResult(IsDBNull(ordinal));

            public bool Read() => IsDataReady = ++_currentRowNumber < _rowCount;

            public Task<bool> ReadAsync(CancellationToken cancellationToken) => Task.FromResult(Read());

            public BufferedDataRecord Initialize([NotNull] DbDataReader reader, [NotNull] IReadOnlyList<ReaderColumn> columns)
            {
                _underlyingReader = reader;
                _columns = columns;

                ReadMetadata();
                InitializeFields();

                while (reader.Read())
                {
                    ReadRow();
                }

                _bools = new BitArray(_tempBools);
                _tempBools = null;
                _nulls = new BitArray(_tempNulls);
                _tempNulls = null;
                _rowCount = _currentRowNumber + 1;
                _currentRowNumber = -1;
                _underlyingReader = null;
                _columns = null;

                return this;
            }

            public async Task<BufferedDataRecord> InitializeAsync(
                [NotNull] DbDataReader reader, [NotNull] IReadOnlyList<ReaderColumn> columns, CancellationToken cancellationToken)
            {
                _underlyingReader = reader;
                _columns = columns;

                ReadMetadata();
                InitializeFields();

                while (await reader.ReadAsync(cancellationToken))
                {
                    ReadRow();
                }

                _bools = new BitArray(_tempBools);
                _tempBools = null;
                _nulls = new BitArray(_tempNulls);
                _tempNulls = null;
                _rowCount = _currentRowNumber + 1;
                _currentRowNumber = -1;
                _underlyingReader = null;
                _columns = null;

                return this;
            }

            private void ReadRow()
            {
                _currentRowNumber++;

                if (_rowCapacity == _currentRowNumber)
                {
                    DoubleBufferCapacity();
                }

                for (var i = 0; i < FieldCount; i++)
                {
                    var column = _columns[i];
                    var nullIndex = _nullOrdinalToIndexMap[i];
                    switch (_columnTypeCases[i])
                    {
                        case TypeCase.Bool:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadBool(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadBool(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Byte:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadByte(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadByte(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Char:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadChar(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadChar(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.DateTime:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadDateTime(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadDateTime(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.DateTimeOffset:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadDateTimeOffset(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadDateTimeOffset(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Decimal:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadDecimal(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadDecimal(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Double:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadDouble(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadDouble(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Float:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadFloat(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadFloat(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Guid:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadGuid(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadGuid(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Short:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadShort(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadShort(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Int:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadInt(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadInt(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Long:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadLong(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadLong(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.SByte:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadSByte(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadSByte(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.UShort:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadUShort(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadUShort(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.UInt:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadUInt(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadUInt(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.ULong:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadULong(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadULong(_underlyingReader, i, column);
                            }

                            break;
                        case TypeCase.Empty:
                            break;
                        default:
                            if (nullIndex != -1)
                            {
                                if (!(_tempNulls[_currentRowNumber * _nullCount + nullIndex] = _underlyingReader.IsDBNull(i)))
                                {
                                    ReadObject(_underlyingReader, i, column);
                                }
                            }
                            else
                            {
                                ReadObject(_underlyingReader, i, column);
                            }

                            break;
                    }
                }
            }

            private void ReadMetadata()
            {
                var fieldCount = _underlyingReader.FieldCount;
                var dataTypeNames = new string[fieldCount];
                var columnTypes = new Type[fieldCount];
                var columnNames = new string[fieldCount];
                for (var i = 0; i < fieldCount; i++)
                {
                    dataTypeNames[i] = _underlyingReader.GetDataTypeName(i);
                    columnTypes[i] = _underlyingReader.GetFieldType(i);
                    columnNames[i] = _underlyingReader.GetName(i);
                }

                _dataTypeNames = dataTypeNames;
                _fieldTypes = columnTypes;
                _columnNames = columnNames;
                _fieldNameLookup = new Lazy<Dictionary<string, int>>(CreateNameLookup, isThreadSafe: false);

                Dictionary<string, int> CreateNameLookup()
                {
                    var index = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
                    for (var i = 0; i < _columnNames.Length; i++)
                    {
                        index[_columnNames[i]] = i;
                    }

                    return index;
                }
            }

            private void InitializeFields()
            {
                var fieldCount = FieldCount;
                if (FieldCount < _columns.Count)
                {
                    throw new InvalidOperationException("The underlying reader doesn't have as many fields as expected.");
                }

                _columnTypeCases = Enumerable.Repeat(TypeCase.Empty, fieldCount).ToArray();
                _ordinalToIndexMap = Enumerable.Repeat(-1, fieldCount).ToArray();
                if (_columns.Count > 0
                    && _columns[0].Name != null)
                {
                    // Non-Composed FromSql
                    var readerColumns = _fieldNameLookup.Value;

                    _indexMap = new int[_columns.Count];
                    var newColumnMap = new ReaderColumn[fieldCount];
                    for (var i = 0; i < _columns.Count; i++)
                    {
                        var column = _columns[i];
                        if (!readerColumns.TryGetValue(column.Name, out var ordinal))
                        {
                            throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name));
                        }

                        newColumnMap[ordinal] = column;
                        _indexMap[i] = ordinal;
                    }

                    _columns = newColumnMap;
                }

                if (FieldCount != _columns.Count)
                {
                    var newColumnMap = new ReaderColumn[fieldCount];
                    for (var i = 0; i < _columns.Count; i++)
                    {
                        newColumnMap[i] = _columns[i];
                    }
                    _columns = newColumnMap;
                }

                for (var i = 0; i < fieldCount; i++)
                {
                    var column = _columns[i];
                    if (column == null)
                    {
                        continue;
                    }

                    var type = column.Type;
                    if (type == typeof(bool))
                    {
                        _columnTypeCases[i] = TypeCase.Bool;
                        _ordinalToIndexMap[i] = _boolCount;
                        _boolCount++;
                    }
                    else if (type == typeof(byte))
                    {
                        _columnTypeCases[i] = TypeCase.Byte;
                        _ordinalToIndexMap[i] = _byteCount;
                        _byteCount++;
                    }
                    else if (type == typeof(char))
                    {
                        _columnTypeCases[i] = TypeCase.Char;
                        _ordinalToIndexMap[i] = _charCount;
                        _charCount++;
                    }
                    else if (type == typeof(DateTime))
                    {
                        _columnTypeCases[i] = TypeCase.DateTime;
                        _ordinalToIndexMap[i] = _dateTimeCount;
                        _dateTimeCount++;
                    }
                    else if (type == typeof(DateTimeOffset))
                    {
                        _columnTypeCases[i] = TypeCase.DateTimeOffset;
                        _ordinalToIndexMap[i] = _dateTimeOffsetCount;
                        _dateTimeOffsetCount++;
                    }
                    else if (type == typeof(decimal))
                    {
                        _columnTypeCases[i] = TypeCase.Decimal;
                        _ordinalToIndexMap[i] = _decimalCount;
                        _decimalCount++;
                    }
                    else if (type == typeof(double))
                    {
                        _columnTypeCases[i] = TypeCase.Double;
                        _ordinalToIndexMap[i] = _doubleCount;
                        _doubleCount++;
                    }
                    else if (type == typeof(float))
                    {
                        _columnTypeCases[i] = TypeCase.Float;
                        _ordinalToIndexMap[i] = _floatCount;
                        _floatCount++;
                    }
                    else if (type == typeof(Guid))
                    {
                        _columnTypeCases[i] = TypeCase.Guid;
                        _ordinalToIndexMap[i] = _guidCount;
                        _guidCount++;
                    }
                    else if (type == typeof(short))
                    {
                        _columnTypeCases[i] = TypeCase.Short;
                        _ordinalToIndexMap[i] = _shortCount;
                        _shortCount++;
                    }
                    else if (type == typeof(int))
                    {
                        _columnTypeCases[i] = TypeCase.Int;
                        _ordinalToIndexMap[i] = _intCount;
                        _intCount++;
                    }
                    else if (type == typeof(long))
                    {
                        _columnTypeCases[i] = TypeCase.Long;
                        _ordinalToIndexMap[i] = _longCount;
                        _longCount++;
                    }
                    else if (type == typeof(sbyte))
                    {
                        _columnTypeCases[i] = TypeCase.SByte;
                        _ordinalToIndexMap[i] = _sbyteCount;
                        _sbyteCount++;
                    }
                    else if (type == typeof(ushort))
                    {
                        _columnTypeCases[i] = TypeCase.UShort;
                        _ordinalToIndexMap[i] = _ushortCount;
                        _ushortCount++;
                    }
                    else if (type == typeof(uint))
                    {
                        _columnTypeCases[i] = TypeCase.UInt;
                        _ordinalToIndexMap[i] = _uintCount;
                        _uintCount++;
                    }
                    else if (type == typeof(ulong))
                    {
                        _columnTypeCases[i] = TypeCase.ULong;
                        _ordinalToIndexMap[i] = _ulongCount;
                        _ulongCount++;
                    }
                    else
                    {
                        _columnTypeCases[i] = TypeCase.Object;
                        _ordinalToIndexMap[i] = _objectCount;
                        _objectCount++;
                    }
                }

                _tempBools = new bool[_rowCapacity * _boolCount];
                _bytes = new byte[_rowCapacity * _byteCount];
                _chars = new char[_rowCapacity * _charCount];
                _dateTimes = new DateTime[_rowCapacity * _dateTimeCount];
                _dateTimeOffsets = new DateTimeOffset[_rowCapacity * _dateTimeOffsetCount];
                _decimals = new decimal[_rowCapacity * _decimalCount];
                _doubles = new double[_rowCapacity * _doubleCount];
                _floats = new float[_rowCapacity * _floatCount];
                _guids = new Guid[_rowCapacity * _guidCount];
                _shorts = new short[_rowCapacity * _shortCount];
                _ints = new int[_rowCapacity * _intCount];
                _longs = new long[_rowCapacity * _longCount];
                _sbytes = new sbyte[_rowCapacity * _sbyteCount];
                _ushorts = new ushort[_rowCapacity * _ushortCount];
                _uints = new uint[_rowCapacity * _uintCount];
                _ulongs = new ulong[_rowCapacity * _ulongCount];
                _objects = new object[_rowCapacity * _objectCount];

                _nullOrdinalToIndexMap = Enumerable.Repeat(-1, fieldCount).ToArray();
                for (var i = 0; i < fieldCount; i++)
                {
                    if (_columns[i]?.IsNullable == true)
                    {
                        _nullOrdinalToIndexMap[i] = _nullCount;
                        _nullCount++;
                    }
                }

                _tempNulls = new bool[_rowCapacity * _nullCount];
            }

            private void DoubleBufferCapacity()
            {
                _rowCapacity <<= 1;

                var newBools = new bool[_tempBools.Length << 1];
                Array.Copy(_tempBools, newBools, _tempBools.Length);
                _tempBools = newBools;

                var newBytes = new byte[_bytes.Length << 1];
                Array.Copy(_bytes, newBytes, _bytes.Length);
                _bytes = newBytes;

                var newChars = new char[_chars.Length << 1];
                Array.Copy(_chars, newChars, _chars.Length);
                _chars = newChars;

                var newDateTimes = new DateTime[_dateTimes.Length << 1];
                Array.Copy(_dateTimes, newDateTimes, _dateTimes.Length);
                _dateTimes = newDateTimes;

                var newDateTimeOffsets = new DateTimeOffset[_dateTimeOffsets.Length << 1];
                Array.Copy(_dateTimeOffsets, newDateTimeOffsets, _dateTimeOffsets.Length);
                _dateTimeOffsets = newDateTimeOffsets;

                var newDecimals = new decimal[_decimals.Length << 1];
                Array.Copy(_decimals, newDecimals, _decimals.Length);
                _decimals = newDecimals;

                var newDoubles = new double[_doubles.Length << 1];
                Array.Copy(_doubles, newDoubles, _doubles.Length);
                _doubles = newDoubles;

                var newFloats = new float[_floats.Length << 1];
                Array.Copy(_floats, newFloats, _floats.Length);
                _floats = newFloats;

                var newGuids = new Guid[_guids.Length << 1];
                Array.Copy(_guids, newGuids, _guids.Length);
                _guids = newGuids;

                var newShorts = new short[_shorts.Length << 1];
                Array.Copy(_shorts, newShorts, _shorts.Length);
                _shorts = newShorts;

                var newInts = new int[_ints.Length << 1];
                Array.Copy(_ints, newInts, _ints.Length);
                _ints = newInts;

                var newLongs = new long[_longs.Length << 1];
                Array.Copy(_longs, newLongs, _longs.Length);
                _longs = newLongs;

                var newSBytes = new sbyte[_sbytes.Length << 1];
                Array.Copy(_sbytes, newSBytes, _sbytes.Length);
                _sbytes = newSBytes;

                var newUShorts = new ushort[_ushorts.Length << 1];
                Array.Copy(_ushorts, newUShorts, _ushorts.Length);
                _ushorts = newUShorts;

                var newUInts = new uint[_uints.Length << 1];
                Array.Copy(_uints, newUInts, _uints.Length);
                _uints = newUInts;

                var newULongs = new ulong[_ulongs.Length << 1];
                Array.Copy(_ulongs, newULongs, _ulongs.Length);
                _ulongs = newULongs;

                var newObjects = new object[_objects.Length << 1];
                Array.Copy(_objects, newObjects, _objects.Length);
                _objects = newObjects;

                var newNulls = new bool[_tempNulls.Length << 1];
                Array.Copy(_tempNulls, newNulls, _tempNulls.Length);
                _tempNulls = newNulls;
            }

            private void ReadBool(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _tempBools[_currentRowNumber * _boolCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<bool>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadByte(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _bytes[_currentRowNumber * _byteCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<byte>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadChar(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _chars[_currentRowNumber * _charCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<char>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadDateTime(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _dateTimes[_currentRowNumber * _dateTimeCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<DateTime>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadDateTimeOffset(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _dateTimeOffsets[_currentRowNumber * _dateTimeOffsetCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<DateTimeOffset>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadDecimal(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _decimals[_currentRowNumber * _decimalCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<decimal>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadDouble(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _doubles[_currentRowNumber * _doubleCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<double>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadFloat(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _floats[_currentRowNumber * _floatCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<float>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadGuid(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _guids[_currentRowNumber * _guidCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<Guid>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadShort(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _shorts[_currentRowNumber * _shortCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<short>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadInt(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _ints[_currentRowNumber * _intCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<int>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadLong(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _longs[_currentRowNumber * _longCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<long>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadSByte(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _sbytes[_currentRowNumber * _sbyteCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<sbyte>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadUShort(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _ushorts[_currentRowNumber * _ushortCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<ushort>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadUInt(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _uints[_currentRowNumber * _uintCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<uint>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadULong(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _ulongs[_currentRowNumber * _ulongCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<ulong>)column).GetFieldValue(reader, _indexMap);
            }

            private void ReadObject(DbDataReader reader, int ordinal, ReaderColumn column)
            {
                _objects[_currentRowNumber * _objectCount + _ordinalToIndexMap[ordinal]] =
                    ((ReaderColumn<object>)column).GetFieldValue(reader, _indexMap);
            }

            private enum TypeCase
            {
                Empty = 0,
                Object,
                Bool,
                Byte,
                Char,
                DateTime,
                DateTimeOffset,
                Decimal,
                Double,
                Float,
                Guid,
                SByte,
                Short,
                Int,
                Long,
                UInt,
                ULong,
                UShort
            }
        }
    }
}
